package com.flow.framework.web.advice;

import com.flow.framework.common.error.SystemErrorCode;
import com.flow.framework.common.exception.CheckedException;
import com.flow.framework.common.exception.ParamCheckedException;
import com.flow.framework.common.exception.UncheckedException;
import com.flow.framework.common.util.verify.VerifyUtil;
import com.flow.framework.core.constant.FrameworkCoreConstant;
import com.flow.framework.core.response.Response;
import com.flow.framework.web.controller.ErrorController;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Path;
import java.util.List;
import java.util.Set;

/**
 * 统一异常处理
 *
 * @author luoguopiao
 * @version 0.0.1
 * @date 2022/12/18
 */
@Slf4j
@RequiredArgsConstructor
@ControllerAdvice
public class WebExceptionAdvice {

    private final ErrorController errorController;

    /**
     * 处理Throwable异常
     *
     * @param request   请求
     * @param throwable 异常
     * @return 模型和视图
     */
    @ExceptionHandler(value = Throwable.class)
    public ModelAndView handleException(HttpServletRequest request, Throwable throwable) {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName(errorController.getErrorPath());
        if (throwable instanceof ParamCheckedException) {
            handleException(modelAndView, request, (ParamCheckedException) throwable);
        } else if (throwable instanceof CheckedException) {
            handleException(modelAndView, request, (CheckedException) throwable);
        } else if (throwable instanceof UncheckedException) {
            handleException(modelAndView, request, (UncheckedException) throwable);
        } else if (throwable instanceof HttpRequestMethodNotSupportedException) {
            handleException(modelAndView, request, (HttpRequestMethodNotSupportedException) throwable);
        } else if (throwable instanceof MissingServletRequestParameterException) {
            handleException(modelAndView, request, (MissingServletRequestParameterException) throwable);
        } else if (throwable instanceof HttpMessageNotReadableException) {
            handleException(modelAndView, request, (HttpMessageNotReadableException) throwable);
        } else if (throwable instanceof MethodArgumentNotValidException) {
            handleException(modelAndView, request, (MethodArgumentNotValidException) throwable);
        } else if (throwable instanceof ConstraintViolationException) {
            handleException(modelAndView, request, (ConstraintViolationException) throwable);
        } else if (throwable instanceof BindException) {
            handleException(modelAndView, request, (BindException) throwable);
        } else {
            log.error("unexpected exception.", throwable);
            Throwable cause = throwable;
            while (true) {
                cause = cause.getCause();
                if (null != cause) {
                    if (cause instanceof ParamCheckedException) {
                        handleException(modelAndView, request, (ParamCheckedException) cause);
                    } else if (cause instanceof CheckedException) {
                        handleException(modelAndView, request, (CheckedException) cause);
                    } else if (cause instanceof UncheckedException) {
                        handleException(modelAndView, request, (UncheckedException) cause);
                    }
                } else {
                    break;
                }
            }
            HttpStatus status = modelAndView.getStatus();
            if (null == status) {
                modelAndView.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
                request.setAttribute(FrameworkCoreConstant.FRAMEWORK_RESPONSE_KEY, Response.failed(SystemErrorCode.API_SERVER_ERROR));
            }
        }
        return modelAndView;
    }

    /**
     * 处理检查异常
     *
     * @param modelAndView 模型和视图
     * @param request      请求
     * @param e            异常
     */
    private void handleException(ModelAndView modelAndView, HttpServletRequest request, CheckedException e) {
        log.error("checked exception occurred.", e);
        request.setAttribute(FrameworkCoreConstant.FRAMEWORK_RESPONSE_KEY, Response.failed(e));
        if (e.getCode() == SystemErrorCode.OBJECT_EXIST_ERROR) {
            modelAndView.setStatus(HttpStatus.NOT_FOUND);
        }
    }

    /**
     * 处理未检查异常
     *
     * @param modelAndView 模型和视图
     * @param request      请求
     * @param e            异常
     */
    private void handleException(ModelAndView modelAndView, HttpServletRequest request, UncheckedException e) {
        log.error("unchecked exception occurred.", e);
        request.setAttribute(FrameworkCoreConstant.FRAMEWORK_RESPONSE_KEY, Response.failed(e));
        if (e.getCode() == SystemErrorCode.OBJECT_EXIST_ERROR) {
            modelAndView.setStatus(HttpStatus.NOT_FOUND);
        }
    }

    /**
     * 处理自定义参数检查异常
     *
     * @param modelAndView 模型和视图
     * @param request      请求
     * @param e            异常
     */
    private void handleException(ModelAndView modelAndView, HttpServletRequest request, ParamCheckedException e) {
        log.error("checked exception occurred.", e);
        request.setAttribute(FrameworkCoreConstant.FRAMEWORK_RESPONSE_KEY, Response.failed(e));
        modelAndView.setStatus(HttpStatus.BAD_REQUEST);
    }


    /**
     * 请求类型异常处理器
     *
     * @param modelAndView 模型和视图
     * @param request      请求
     * @param e            异常
     */
    private void handleException(ModelAndView modelAndView, HttpServletRequest request, HttpRequestMethodNotSupportedException e) {
        log.error("request method not supported exception occurred.", e);
        request.setAttribute(FrameworkCoreConstant.FRAMEWORK_RESPONSE_KEY, Response.failed(SystemErrorCode.API_REQUEST_METHOD_ERROR));
        modelAndView.setStatus(HttpStatus.METHOD_NOT_ALLOWED);
    }

    /**
     * 参数异常处理器
     *
     * @param modelAndView 模型和视图
     * @param request      请求
     * @param e            异常
     */
    private void handleException(ModelAndView modelAndView, HttpServletRequest request, MissingServletRequestParameterException e) {
        log.error("missing servlet request parameter exception occurred.", e);
        request.setAttribute(FrameworkCoreConstant.FRAMEWORK_RESPONSE_KEY, Response.failed(SystemErrorCode.API_UNKNOWN_PARAMS_ERROR));
        modelAndView.setStatus(HttpStatus.BAD_REQUEST);
    }

    /**
     * 缺少请求体异常处理器
     *
     * @param modelAndView 模型和视图
     * @param request      请求
     * @param e            异常
     */
    private void handleException(ModelAndView modelAndView, HttpServletRequest request, HttpMessageNotReadableException e) {
        log.error("http message not readable exception occurred.", e);
        request.setAttribute(FrameworkCoreConstant.FRAMEWORK_RESPONSE_KEY, Response.failed(SystemErrorCode.API_REQUEST_BODY_ERROR));
        modelAndView.setStatus(HttpStatus.BAD_REQUEST);
    }

    /**
     * 处理请求参数格式错误@RequestBody上validate失败后抛出的异常是MethodArgumentNotValidException异常
     *
     * @param modelAndView 模型和视图
     * @param request      请求
     * @param e            异常
     */
    private void handleException(ModelAndView modelAndView, HttpServletRequest request, MethodArgumentNotValidException e) {
        BindingResult bindingResult = e.getBindingResult();
        StringBuilder detailMsgBuilder = new StringBuilder();
        List<ObjectError> objectErrors = bindingResult.getAllErrors();
        if (!VerifyUtil.isEmpty(objectErrors)) {
            for (ObjectError objectError : objectErrors) {
                if (objectError instanceof FieldError) {
                    FieldError fieldError = (FieldError) objectError;
                    String fieldName = fieldError.getField();
                    detailMsgBuilder
                            .append(fieldName)
                            .append(": ")
                            .append(fieldError.getDefaultMessage())
                            .append("\n");
                } else {
                    String objectName = objectError.getObjectName();
                    detailMsgBuilder.append(objectName)
                            .append(": ")
                            .append(objectError.getDefaultMessage())
                            .append("\n");
                }
            }
        }
        log.error("method argument not valid exception occurred, error detail : {} ", detailMsgBuilder.toString(), e);
        request.setAttribute(FrameworkCoreConstant.FRAMEWORK_RESPONSE_KEY, Response.failed(SystemErrorCode.API_UNKNOWN_PARAMS_ERROR));
        modelAndView.setStatus(HttpStatus.BAD_REQUEST);
    }

    /**
     * 处理请求参数格式错误@RequestParam上加校验注解并在控制器上加@Validate失败后抛出的异常是javax.validation.ConstraintViolationException
     *
     * @param modelAndView 模型和视图
     * @param request      请求
     * @param e            异常
     */
    private void handleException(ModelAndView modelAndView, HttpServletRequest request, ConstraintViolationException e) {
        Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
        StringBuilder detailMsgBuilder = new StringBuilder();
        if (!CollectionUtils.isEmpty(violations)) {
            for (ConstraintViolation constraintViolation : violations) {
                Path propertyPath = constraintViolation.getPropertyPath();
                String param = propertyPath.toString();
                detailMsgBuilder
                        .append(param)
                        .append(": ")
                        .append(constraintViolation.getMessage())
                        .append("\n");
            }
        }
        log.error("constraint violation exception occurred, error message : {} ", detailMsgBuilder.toString(), e);
        request.setAttribute(FrameworkCoreConstant.FRAMEWORK_RESPONSE_KEY, Response.failed(SystemErrorCode.API_UNKNOWN_PARAMS_ERROR));
        modelAndView.setStatus(HttpStatus.BAD_REQUEST);
    }

    /**
     * 处理Get请求中验证路径中请求实体校验失败后抛出的异常是BindException
     *
     * @param modelAndView 模型和视图
     * @param request      请求
     * @param e            异常
     */
    private void handleException(ModelAndView modelAndView, HttpServletRequest request, BindException e) {
        BindingResult bindingResult = e.getBindingResult();
        FieldError fieldError = bindingResult.getFieldError();
        String errorMessage = null;
        String errorField = null;
        if (fieldError != null) {
            errorMessage = fieldError.getDefaultMessage();
            errorField = fieldError.getField();
        }
        log.error("bind exception occurred, error field name: {}, error message : {} ", errorField, errorMessage, e);
        request.setAttribute(FrameworkCoreConstant.FRAMEWORK_RESPONSE_KEY, Response.failed(SystemErrorCode.API_UNKNOWN_PARAMS_ERROR));
        modelAndView.setStatus(HttpStatus.BAD_REQUEST);
    }
}
